package br.mikhas.refractor.method.invoker;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

public final class MethodInvokerFactory {

	private static final ClassPool pool;
	private static CtClass objectCt;
	private static CtClass objectArrCt;
	private static CtClass invokerCt;
	private static MethodInvokerFactory instance;
	private int count = 0;

	private Map<Method, MethodInvoker> invokers;

	static {
		pool = ClassPool.getDefault();
	}

	private static void initialize() {
		try {
			objectCt = pool.get("java.lang.Object");
			objectArrCt = pool.get("java.lang.Object[]");
			invokerCt = pool.get(MethodInvoker.class.getName());
		} catch (Exception e) {
			throw new RuntimeException(
					"Could not initialize the MethodInvokerFactory", e);
		}
	}

	public static MethodInvokerFactory getDefault() {
		if (instance == null) {
			initialize();
			instance = new MethodInvokerFactory();
		}

		return instance;
	}

	private MethodInvokerFactory() {
		invokers = new HashMap<Method, MethodInvoker>();
	}

	public MethodInvoker getFor(Method method) {
		int modifiers = method.getModifiers();
		if (Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)
				|| Modifier.isAbstract(modifiers)) {
			throw new RuntimeException(
					"Invokers may be created on for non-static public methods.");
		}

		MethodInvoker methodInvoker = invokers.get(method);

		if (methodInvoker == null) {
			methodInvoker = this.createInvokerFor(method);
			invokers.put(method, methodInvoker);
		}

		return methodInvoker;
	}

	@SuppressWarnings("unchecked")
	private MethodInvoker createInvokerFor(Method method) {
		CtClass invokerClass = pool.makeClass("Invoker$" + (count++));

		invokerClass.setModifiers(javassist.Modifier.PUBLIC);
		invokerClass.addInterface(invokerCt);

		try {
			addInvokeMethod(invokerClass, method);
			Class<? extends MethodInvoker> invokerCls = invokerClass.toClass();

			return invokerCls.newInstance();

		} catch (Exception e) {
			throw new RuntimeException(
					"Could not create invoker for " + method, e);
		}
	}

	private void addInvokeMethod(CtClass invokerClass, Method method)
			throws CannotCompileException, NotFoundException {
		Class<?> declaringClass = method.getDeclaringClass();
		String declaringName = declaringClass.getName();
		String methodName = method.getName();
		Class<?>[] parameters = method.getParameterTypes();

		CtMethod invoke = new CtMethod(objectCt, "invoke", new CtClass[] {
				objectCt, objectArrCt }, invokerClass);

		invoke.setModifiers(javassist.Modifier.PUBLIC);

		StringBuilder builder = new StringBuilder();

		builder.append('{');
		// Sets the target

		builder.append("try {");

		builder.append(declaringName).append(" target = (").append(
				declaringName).append(") $1;");

		if (method.getReturnType() != Void.class)
			builder.append("return ");

		builder.append("target.").append(methodName).append('(');

		for (int i = 0; i < parameters.length; i++) {
			if (i > 0)
				builder.append(',');
			// Cast first
			builder.append('(').append(parameters[i].getName()).append(')');
			builder.append("$2[").append(i).append(']');
		}

		builder.append(");");

		if (method.getReturnType() == Void.class)
			builder.append("return null;");

		builder
				.append("}catch(java.lang.Throwable t){throw new RuntimeException(t);}");

		builder.append('}');

		invoke.setBody(builder.toString());
		invokerClass.addMethod(invoke);
	}
}
